Table of Contents

1.Обзор системы | 2. Мой первый проект | 3. Модель | 4. Представление | 5. Табличное представление | 6. Объектно-реляционное связывание | 7. Контроллеры | 8. Приложение | 9. Расширенные возможности

Глава 3: Модель (Model)

Слой (layer), называемый Моделью (Model), в объектно-ориентированном приложении содержит бизнес-логику. Которая обычно состоит из структур данных, вычислительной или прикладной логики, проверок и прочего, ассоциированных с данными.
OpenXava - это ориентированный на модель фреймворк, в котором модель имеет наибольшее значение, и все остальное (например, пользовательский интерфейс) зависит от неё или является её производным.
В последних версиях OpenXava для специфицирования модели используются Plain Old Java Objects (POJO) - простые Java классы + используются аннотации, которые стали доступны в Java5. Также возможно определение модели с помощью XML, которое было реализованно в более раних версиях OX.
Из описанной вами модели OpenXava способен создавать полностью функциональные веб приложения.

Бизнес-компонент (Business Component)

Фундаментом OpenXava является понятие бизнес-компонента (business component). Бизнес-компонент опреляется с помощью Java объекта, именнуемого в OX сущностью (Entity). Данный класс является обычным EJB3 компонентом, или другими словами, классом POJO с аннотациями, реализованными в соответствии со стандартом Java Persistence API (JPA).
JPA - это стандарт Java для постоянного хранения объектов (persistence). В первую очередь он предназначен для сохранения и извлечения состояния объектов Java в/из базы данных. Если вы имете опыт разработки JPA с использованием POJO-объектов, считайте, что вы уже умеете разрабатывать приложения OpenXava.
Давайте поймем, что мы можем реализовать, определив Бизнес-компонент, который на самом деле является простым Java классом:
  • Model (Модель): Структуры данных, проверки, вспомогательные вычисление и многое другое.
  • View(Представление): Это, как модель будет отображаться пользователю.
  • Tabular data(Табличное представление или списки объектов): Специальное представление для презентации списка объектов в режиме списка (по-другому, таблицы).
  • Object/relational mapping(Правила сохранения объектов в базу данных): Описывает, как будет сохраняться и извлекаться состояние объектов в/из базы данных.
Данная глава посвещена определению модели данных.

Сущность (Entity)

Для того, чтобы определить модель, нужно начать с создания обычного класса Java. Дополнительная информация, необходимая для OX, определяется с помощью аннотаций. OpenXava поддерживает аннотации JPA и Hibernate Validator. Кроме этого, OpenXava определяет целый ряд собственных аннотаций для управления визуализацией бизнес-компонентов и другой функциональностью.
Аббревиатура JPA в тексте данной главы указывает аннонтации Java Persistent API, аббревиатурой HV обозначены аннотации Hibernate Validator, а аббревиатура OX указывает аннотации OpenXava.
Синтаксическое определение entity следующее:
 @Entity // 1
 @EntityValidator // 2
 @RemoveValidator // 3
 public class EntityName { // 4
 // Properties // 5
 // References // 6
 // Collections // 7
 // Methods // 8
 // Finders // 9
 // Callback methods // 10
 }
  1. @Entity (JPA, одно вхождение, обязательный элемент): Указывает, что данный класс является сущностью JPA, другими словами, экземпляры данного класса управляются системой постоянного хранения объектов (persistent objects).
  2. @EntityValidator (OX, несколько вхождений, необязательный элемент): Выполяет проверку на уровене модели. В данной аннотации можно задать значение любых нескольких свойств модели. Для проверки значения одного свойства мы рекомендуем использовать проверку на уровне конркетного свойства (property level validator).
  3. @RemoveValidator (OX, несколько вхождений, необязательный элемент): Данная проверка выполняется перед удалением объекта. Позволяет запретить удаление объекта.
  4. Class declaration: Обычное определение класса Java. Кроме прочего, можно использовать наследование других классов и реализацию интерфейсов (служебные слова Java extends и implements).
  5. Properties: Обычные свойства (private переменные-члены класса, имеющие Get/Set методы) Java. Значения свойств определяет состояние объекта.
  6. References: Ссылки на другие сущности.
  7. Collections: Коллекции ссылок на другие сущности.
  8. Methods: Обычные методы Java класса, могут содержать бизнес-логику (business logic).
  9. Finders: Методы выборки объектов из базы данных(Finder). Это статические методы (static), позволяющие реализовать сложный поиск объектов в БД, используя рассширенные возможности JPA.
  10. Callback methods: Методы обратного вызова JPA (JPA callbacks methods) для добавления собственной логики, которая будет задествована при вставке, изменении или удалении объектов в базе данных.

Свойства сущности (Properties)

Свойство представляет собой один отдельный аттрибут сущности. В OX свойство реализуется как переменная-член класса Java. От обычной переменной члена класса свойство отличается тем, что свойство обычно имеет область видимости private, а также реализованы методы get/set (которые называются геттеры и сеттеры на программистском сленге) для чтения и установки значения свойства. Например, свойство X будет иметь методы setX и getX. Иногда свойство имеет только один из методов, например, если это вычислимое свойство, но сейчас мы не будем рассматривать такой случай.
Синтаксическое определение свойства в OX следующее:
 @Stereotype // 1
 @Column(length=) @Max @Length(max=) @Digits(integerDigits=) // 2
 @Digits(fractionalDigits=) // 3
 @Required @Min @Range(min=) @Length(min=) // 4
 @Id // 5
 @Hidden // 6
 @SearchKey // 7
 @Version // 8
 @DefaultValueCalculator // 9
 @PropertyValidator // 10
 private type propertyName; // 11
 public type getPropertyName() { ... } // 11
 public void setPropertyName(type newValue) { ... } // 11
  1. @Stereotype (OX, необязательный элемент): OX уже имеет готовые стереотипы для некоторых распространённых свойств, например, адрес e-mail. С помощью данной аннотации вы можете указать такой готовый стеретип.
  2. @Column(length=) (JPA), @Max (HV), @Length(max=) (HV), @Digits(integerDigits=) (HV, необязательный элемент, предполагается, что одновременно будет использоваться только одна из приведённых аннотаций): Аттрибуты length= или max= определяет длину значения свойства в символах. В случае аннотации @Max max= определяет максимальное значение свойства. Применяется для уточнения отображения свойства в пользовательском интерфейсе. Если вы не задаете размер или максимальное значение, используется значение по-умолчанию, определенное для типа или стереотипа в файле default-size.xml конфигурации.
  3. @Digits(fractionalDigits=) (HV, необязательный элемент): Размер дробной части числа. Применимо только для числовых свойств. Если не задается, используется размер по-умолчанию из файла default-size.xml конфигураци.
  4. @Required (OX), @Min (HV), @Range(min=) (HV), @Length(min=) (HV) (Необязательный элемент. Предполагается, что одновременно будет использоваться только одна из преведённых аннотаций): Указывает, что данное свойство является обязательным. В случае @Min (минимальное значение), @Range (диапазон) или @Length (длина) необходимо указать значение больше 0 для аттрибута min, если необходимо, чтобы значение свойства было задано пользователем. По-умолчанию, ключевые свойства (key properties) являются обязательными (required) и скрытыми (hidden) (начиная с версии v2.1.3). Остальные свойства, по-умолчанию, наоборот, необязательны и видимы. При вставке данных в БД OpenXava проверяет - все ли обязательные свойства в классе присутствуют. При этом, если хотя бы одно из обязательных свойств не заполнено, то сохранение не выполняется и пользователю выводится сообщение об ошибке (validation error list). Логика определения "присутствия" значения свойства (т.е. заполнено свойство или нет) может быть переопределена созданием файла validators.xml внутри проекта. В качестве примера и ознакомления с форматом файла посмотрите основной файл валидации OpenXava/xava/validators.xml. Обращаем ваше внимание, что @Required - это констрейн Hibernate (Hibernate Validator constraint) (начиная с версии v3.0.1).
  5. @Id (JPA, необязательный элемент): Указывает, что данное свойство является частью ключа (являлось ключевым свойством - key property). Требуется, чтобы сущность имела хотя бы одно ключевое свойство (или ключом может являтся ссылка (reference)). Комбинация ключевых свойств (key properties) и ключевых ссылок (key references) должна быть спроецированна (mapping) на группу столбцов в таблице(ах) базы данных, которые не могут иметь дуплицированных значения. Обычно, это первичный ключ (primary key) таблицы.
  6. @Hidden (OX, необязательный элемент): Указав, что свойство скрытое, вы исключаете его из автоматической генерации пользовательского интерфейса. Естественно, что java код никуда не пропадает, и вы можете использовать данной свойство программно. Также, если вы явно включите свойство в представление, то оно будет отображено в данном представлении.
  7. @SearchKey (OX, необязательный элемент): Свойства с аттрибутом "Поисковый ключ (search key)" пользователям для поиска. В пользовательском интерфейсе OX предусмотрены специальные редактируемые поля для ввода значения, которое будет использоваться для поиска. По-умолчанию OpenXava использует для поиска ключевые свойства (с аттрибутом @Id). Если же ключевые свойства скрыты (hidden), то ,OpenXava использует первое свойство в представлении (view). Применив @SearchKey, вы точно указываете свойства, которые будут использоваться для поиска.
  8. @Version (JPA, необязательный элемент): Свойство с аттибутом "version" используется логикой оптимистического управления транзакциями (optimistic concurrency control). Если вы хотите активировать функцию "control concurrency" - все,что нужно, иметь свойство с аттрибутом @Version для данной сущности. В одной сущности может быть использовано только одно свойство с аттрибутом @Version. Разрешенные типы для использования с аттрибутом @Version: int, Integer, short, Short, long, Long, Timestamp. Такие свойства также являются скрытыми (см. @hidden).
  9. @DefaultValueCalculator (OX, одно вхождение, необязательный элемент): Реализует логику вычисления начального (default) значения для свойства. OpenXava требует, чтобы свойство с аттрибутом @DefaultValueCalculator имело метод для установки значения (setter).
  10. @PropertyValidator (OX, возможно несколько вхождений, необязательный элемент): Определяет логику проверки значения свойства (валидации), которая выполняется перед изменением или созданием объекта, который содержит данное свойство.
  11. Определение свойства (Property declaration): Обычное для Java определение свойства с get- и set- методами для доступа к значению свойства (getters и setters). Если вы хотите создать вычислимое свойство (calculated property), то для это нужно только определить get- метод без собственно определения переменной класса и set- метода. В качестве типа данных можно использовать любой разрешенный в JPA тип.

Стереотип (Stereotype)

Стереотип (@Stereotype) позволяет упростить определение и сопровождение сложного поведения свойств. В большом приложении обычно во многих сущностях используются похожие или идентичные свойства. Стереотип позволяет избежать необходимость описывать / реализовывать все аспекты поведения и логики для каждого отдельного свойства. Все похожие свойства могут быть реализованы на базе одного стереотипа. Разработчик может создать свой или задействовать уже реализованный стереотип. OX уже имеет готовые стереотипы для некоторых распространённых случаев, например: адрес e-mail, название (name), комментарий (comment), описание (description) и т.д. OX реализует поведение по-умолчанию для большинства стандартных типов Java, которые поддерживаются JPA. Но если вы хотите построить действительно профессиональный пользовательский интерфейс - например, настроить проверку корректности значений (validators), размеры по-умолчанию (default sizes), редактор (visual editors) и многое другое специальным образом для конкретных свойств, то сделать это пожет использование стереотипов. Ничто не мешает вам создать собственные стереотипы, например, NAME, MEMO или DESCRIPTION и использовать их для ваших свойств.
Ниже приведен список стереотипов, входящих в стандартную поставку OpenXava:
  • DINERO, MONEY - деньги
  • FOTO, PHOTO, IMAGEN, IMAGE - изображения
  • TEXTO_GRANDE, MEMO, TEXT_AREA
  • ETIQUETA, LABEL - метка
  • ETIQUETA_NEGRITA, BOLD_LABEL - метка, выделенная жирным шрифтом
  • HORA, TIME - время
  • FECHAHORA, DATETIME - дата и время в одном поле
  • GALERIA_IMAGENES, IMAGES_GALLERY (инструкции по установке) - "Галлерея изображений"
  • RELLENADO_CON_CEROS, ZEROS_FILLED - поле с заполнением нулями
  • TEXTO_HTML, HTML_TEXT (text with editable format) - текст
  • IMAGE_LABEL, ETIQUETA_IMAGEN - изображение, зависящее от значения свойства
  • EMAIL - адрес электронной почты
  • TELEFONO, TELEPHONE - телефон
  • WEBURL - веб URL
  • IP - ip адрес
  • ISBN - International Serial Book Number
  • TARJETA_CREDITO, CREDIT_CARD - номер кредитной карты
  • LISTA_EMAIL, EMAIL_LIST - список адресов электронной почты

Давайте попробуем создать свой собственный стереотип. Предположим, что вы хотите создать новый стереотип PERSON_NAME, который предназначен для представления Ф.И.О. человека.
Откройте на редактирование (если не существует, то создайте) файл editors.xml в папке xava ваше проекта. Добавьте в файл:
 <editor url="personNameEditor.jsp">
 <for-stereotype stereotype="PERSON_NAME"/>
 </editor>
Этим вы определили редактор, который будет использоваться для отображения и редактирования свойств, относящихся к стереотипу PERSON_NAME.
Также вы можете определить размер для свойств, использующих данный стереотип. Делается это в файле default-size.xml вашего проекта:
 <for-stereotype name="PERSON_NAME" size="40"/>
В этом примере для свойств со стереотипом PERSON_NAME будет использоваться размер 40, если иного не определено в аттрибутах самого свойства.
Иногда требуется изменить правила валидации (validator) для стереотипа. Для этого необходимо в файл validators.xml вашего проекта добавить следующее:
 <required-validator>
 <validator-class class="org.openxava.validators.NotBlankCharacterValidator"/>
 <for-stereotype stereotype="PERSON_NAME"/>
 </required-validator>
В этом примере для стереотипа PERSON_NAME в качестве валидатора указан класс NotBlankCharacterValidator из пакета org.xava.validators.
Теперь все готово, чтобы определить свойство с использованием стереотипа PERSON_NAME:
 @Stereotype("PERSON_NAME")
 private String name;
В нашем случае значение 40 задает размер, String - это тип и проверка NotBlankCharacterValidator выполняется, если данное свойство является обязательным.

Стереотип "Галлерея изображений" (IMAGES_GALLERY stereotype)

OX имеет встроенную поддержку галерей изображений. Для создания галереи изображений вам необходимо создать свойство сущности, используя стереотип IMAGES_GALLERY stereotype. Пример ниже:
 @Stereotype("IMAGES_GALLERY")
 private String photos;
Столбец photos в базе данных должен иметь тип String и размер 32 символа (VARCHAR(32)).
Мы закончили определение свойства, использующего стереотип IMAGES_GALLERY.
Для того, чтобы создать работающее приложение, нам понадобиться выполнить еще несколько простых шагов.
Первое, что нужно сделать, - это создать таблицу в базе данных для хранения изображений:
 CREATE TABLE IMAGES (
 ID VARCHAR(32) NOT NULL PRIMARY KEY,
 GALLERY VARCHAR(32) NOT NULL,
 IMAGE BLOB);
 CREATE INDEX IMAGES01
 ON IMAGES (GALLERY);
 
Тип BLOB поддерживают не все базы данных. Поэтому в качестве типа данных для столбца IMAGE вам необходимо выбрать тип данных, наиболее подходящий для хранения массива байтов (byte []).
Далее нужно внести изменения в persistence/hibernate.cfg.xml вашего проекта:
 <hibernate-configuration>
 <session-factory>
 ...
 <mapping resource="GalleryImage.hbm.xml"/>
 ...
 </session-factory>
 </hibernate-configuration>
Теперь вы можете использовать стереотип IMAGES_GALLERY в вашем приложении.

Параллельное исполнение запросов и свойство Version (Concurrency and version property)

Под параллельным исполнением запросов подразумевается возможность приложения обеспечивать нескольким пользователям одновременно изменять данные. OpenXava использует оптимистическую модель управления транзакциями JPA. При использовании такой модели записи в базе данных не блокируются, что позволяет получить парарлельную работу пользователей без потери целостности данных.
Например, пусть пользователь A прочитал запись. После этого пользователь B прочитал ту же запись, модифицировал ее и сохранил изменения в БД. В том случае, если пользователь A попытается сохранить свои изменения, он получит сообщение об ошибке и будет вынужден заново загрузить данные и повторить свои изменения.
Чтобы задействовать оптимистическую модель управления транзакциями, вам необходимо определить свойство с аттрибутом @Version в вашей сущности. Пример определения такого свойства:
 @Version
 private int version;
Данное свойство предназначено для использования JPA, поэтому ваше приложение или пользователи не должны использовать данное свойство явным способом.

Перечислимые типы (Enums)

OpenXava поддерживает введённые в Java 5 перечислимые типы enums. Используя enum, вы можете определить свойство, которое может принимать только значения из фиксированного набора, заданного при объявлении enum.
Пример:
 private Distance distance;
 public enum Distance { LOCAL, NATIONAL, INTERNATIONAL };
 
В данном примере свойство distance может принимать только следующие значения: LOCAL, NATIONAL or INTERNATIONAL, и если вы не указали аттрибут @Required, то значение null также допустимо.
В текущей реализации пользовательского интерфейса такое свойство представляется в виде combo. Текст метки для каждого значения берётся из файлов i18n.
В базе данных по умолчанию значение сохраняется как integer (в нашем примере, 0 - это LOCAL, 1 - NATIONAL, 2 - INTERNATIONAL и null, если значение не определено). Тем не менее, вы можете использовать другой тип, что удобно, если вы работаете с уже существующей базой данных. Более подробная информация дана в другой главе.

Вычисляемые свойства (Calculated properties)

Вычислимые свойства не могут быть изменены явным способом (т.к. имеют только метод установки значения getter) и не сохраняются в базе данных (соответственно, не подлежат проецированию на столбцы в БД).
Ниже приведен пример определения вычислимого свойства:
 @Depends("unitPrice") // 1
 @Max(9999999999L) // 2
 public BigDecimal getUnitPriceInPesetas() {
 if (unitPrice == null) return null;
 return unitPrice.multiply(new BigDecimal("166.386")).setScale(0, BigDecimal.ROUND_HALF_UP);
 }
 
Использовать выше представленное определение можно так:
 Product product = ...
 product.setUnitPrice(2);
 BigDecimal result = product.getUnitPriceInPesetas();
 
Result будет равен 332.772.
Свойство unitPriceInPesetas будет отображаться пользователю нередактируемым. Визуальная длина данного поля будет 10, что определяется путем задания аттрибута @Max(9999999999L) (2). Т.к. мы указали аттрибут @Depends("unitPrice") (1), то если пользователь изменит значение свойства unitPrice в пользовательском интерфейсе, то значение свойства unitPriceInPesetas будет пересчитано и новое значение будет отображено пользователю.
В getter-методе вычислимого свойства вы можете возможности JDBC, как показано в этом примере:
 @Max(999)
 public int getDetailsCount() {
 // An example of using JDBC
 Connection con = null;
 try {
 con = DataSourceConnectionProvider.getByComponent("Invoice").getConnection(); // 1
 String table = MetaModel.get("InvoiceDetail").getMapping().getTable();
 PreparedStatement ps = con.prepareStatement("select count(*) from " + table +
 " where INVOICE_YEAR = ? and INVOICE_NUMBER = ?");
 ps.setInt(1, getYear());
 ps.setInt(2, getNumber());
 ResultSet rs = ps.executeQuery();
 rs.next();
 Integer result = new Integer(rs.getInt(1));
 ps.close();
 return result;
 }
 catch (Exception ex) {
 log.error("Problem calculating details count of an Invoice", ex);
 // You can throw any runtime exception here
 throw new SystemException(ex);
 }
 finally {
 try {
 con.close();
 }
 catch (Exception ex) {
 }
 }
 }
 
Прямое использование JDBC выглядит не очень модно, но позволяет улучшить производительность приложения. Класс DataSourceConnectionProvider дает возможность получить соединение JDBC, ассоциированное с источником данных для указанной сущности (в нашем случае это Invoice). Данный класс был добавлен в OX для удобства разработки, но вы можете подключаться к JDBC с использованием JNDI или любым иным удобным вам способом. На самом деле вы можете использовать любой код Java для расчета значения вычислимого свойства.
Если вы используете доступ на базе свойств (property-based access),то вам необходимо добавить аннотацию @Transient для вычислимого свойства. Пример:
 private long number;
 
 @Id @Column(length=10) // You annotated the getter,
 public long getNumber() { // so JPA will use property-base access for your class
 return number;
 }
 public void setNumber(long number) {
 this.number = number;
 }
 
 @Transient // You have to annotate as Transient your calculated property
 public String getZoneOne() { // because you are using property-based access
 return "In ZONE 1";
 }
 

Вычислитель исходного значения (Default value calculator)

Используя аттрибут @DefaultValueCalculator, вы можете задать для свойства класс, который будет определять исходное значение для данного свойства. Такой класс в OX называется калькулятором или вычислителем исходного значения. Например:
 @DefaultValueCalculator(CurrentYearCalculator.class)
 private int year;
 
В этом случае, когда пользователь попытается создать новый экземпляр сущности, он увидит, что значение для свойства "year" уже задано. Далее, если это необходимо, пользователь может изменить данное значение. Логика для вычисления исходного значения определена в классе CurrentYearCalculator:
 package org.openxava.calculators;
 
 import java.util.*;
 
 /**
 * @author Javier Paniza
 */
 public class CurrentYearCalculator implements ICalculator {
 
 public Object calculate() throws Exception {
 Calendar cal = Calendar.getInstance();
 cal.setTime(new java.util.Date());
 return new Integer(cal.get(Calendar.YEAR));
 }
 
 }
 
Существуют и другие варианты использования @DefaultValueCalculator:
 @DefaultValueCalculator(
 value=org.openxava.calculators.StringCalculator.class,
 properties={ @PropertyValue(name="string", value="GOOD") }
 )
 private String relationWithSeller;
В данном случае для вычисления исходного значения OpenXava создает экземпляр StringCalculator, устанавливает значение "GOOD" для свойства string экземпляра StringCalculator, и в конце вызывает его метод calculate() для вычисления значения свойства relationWithSeller. Как вы увидели, используя аннотацию @PropertyValue, вы можете создавать калькуляторы, подходящие для многократного использования.
Анотация @PropertyValue также позволяет использовать значения свойств других сущностей, например:
 @DefaultValueCalculator(
 value=org.openxava.test.calculators.CarrierRemarksCalculator.class,
 properties={
 @PropertyValue(name="drivingLicenceType", from="drivingLicence.type")
 }
 )
 private String remarks;
В данном случае OpenXava заполняет свойство drivingLicenceType посредством использования класса CarrierRemarksCalculator значением отображаемого свойства type из ссылочной сущности drivingLicence. Обратите внимание, что аттрибут from поддерживает ссылки на свойства (reference.property).
Также можно использовать @PropertyValue без from или value:
 @DefaultValueCalculator(value=DefaultProductPriceCalculator.class, properties=
 @PropertyValue(name="familyNumber")
 )
В данном случае OpenXava использует значение отображаемого свойства familyNumber и вставляет значение в свойство familyNumber калькулятора. Выражение @PropertyValue(name="familiyNumber") эквивалентно @PropertyValue(name="familiyNumber", from="familyNumber").
Внутри класса-калькулятора вы можете использовать возможности JDBC напрямую. Вот пример:
 @DefaultValueCalculator(value=DetailsCountCalculator.class,
 properties= {
 @PropertyValue(name="year"),
 @PropertyValue(name="number"),
 }
 )
 private int detailsCount;
 
Пример калькулятора:
 package org.openxava.test.calculators;
 
 import java.sql.*;
 
 import org.openxava.calculators.*;
 import org.openxava.util.*;
 
 /**
 * @author Javier Paniza
 */
 public class DetailsCountCalculator implements IJDBCCalculator { // 1
 
 private IConnectionProvider provider;
 private int year;
 private int number;
 
 public void setConnectionProvider(IConnectionProvider provider) { // 2
 this.provider = provider;
 }
 
 public Object calculate() throws Exception {
 Connection con = provider.getConnection();
 try {
 PreparedStatement ps = con.prepareStatement(
 "select count(*) from XAVATEST.INVOICEDETAIL “ +
 “where INVOICE_YEAR = ? and INVOICE_NUMBER = ?");
 ps.setInt(1, getYear());
 ps.setInt(2, getNumber());
 ResultSet rs = ps.executeQuery();
 rs.next();
 Integer result = new Integer(rs.getInt(1));
 ps.close();
 return result;
 }
 finally {
 con.close();
 }
 }
 
 public int getYear() {
 return year;
 }
 
 public int getNumber() {
 return number;
 }
 
 public void setYear(int year) {
 this.year = year;
 }
 
 public void setNumber(int number) {
 this.number = number;
 }
 
 }
Чтобы использовать прямые вызове JDBC в калькуляторе, класс должен реализовывать интерфейс IJDBCCalculator (1), включая определение переменную-член класса IConnectionProvider (2).
Уже реализованные калькуляторы вы найдете в пакете org.openxava.calculators из поставки OpenXava.

Исходные значения при создании (Default values on create)

Вы можете определить значение свойства, которое будет вычисленно и использоваться только при создании экземпляра сущности (при вставке записи в БД).
Значения свойств первичного ключа обычно вычисляются с применением JPA.
Например, автоикрементное значения для первичного ключа реализуется так:
 @Id @Hidden
 @GeneratedValue(strategy=GenerationType.IDENTITY)
 private Integer id;
 
Можно реализовать варианты заполнения ключевых столбцов, например, использование SEQUENCE объектов БД(также реализация по стандарту JPA):
 @SequenceGenerator(name="SIZE_SEQ", sequenceName="SIZE_ID_SEQ", allocationSize=1 )
 @Hidden @Id @GeneratedValue(strategy=GenerationType.SEQUENCE, generator="SIZE_SEQ")
 private Integer id;
 
Для реализации уникального идентификатора UUID (тип String размером 32 символов), можно использовать расширение Hibernate стандарта JPA:
 @Id @GeneratedValue(generator="system-uuid") @Hidden
 @GenericGenerator(name="system-uuid", strategy = "uuid")
 private String oid;
 
Более подробную информацию об аннотации @GeneratedValues вы найдете в главе 9.1.9 спецификации стандарта JPA 1.0 (часть JSR-220).

Если вам необходимо реализовать нестандартную логику для вычисления значения ключевого свойства при создании объекта или вам нужно генерировать значения неключевого свойства, то вместо @GeneratedValue, вы вам придется использовать следующую конструкцию:
 @PrePersist
 private void calculateCounter() {
 counter = new Long(System.currentTimeMillis()).intValue();
 }
 
Анотация @PrePersist (JPA) определяет метод, который будет выполнен перед первой вставкой данных в БД. В данном методе вы можете реализовать любую самую экзотическую логику для вычисления значения любых свойств: и ключевых, и неключевых.

Проверка значения свойства (Property validator)

Анонтация @PropertyValidator позволяет указать класс (назовём его валидатором), который будет выполнять логику проверки значения свойства перед тем, как данное значение присвоить свойству. Свойство может одновременно иметь несколько проверок и, соответственно, конструкций @PropertyValidator:
 @PropertyValidators ({
 @PropertyValidator(value=ExcludeStringValidator.class, properties=
 @PropertyValue(name="string", value="MOTO")
 ),
 @PropertyValidator(value=ExcludeStringValidator.class, properties=
 @PropertyValue(name="string", value="COCHE"),
 onlyOnCreate=true
 )
 })
 private String description;
 
Техника конфигурирования валидатора примерно такая же как для калькулятора. За исключением того, что в аннотации @PropertyValue нет аттрибута from и вам нужно всегда использовать аттрибут value.
Указав аттрибут onlyOnCreate=”true”, вы создадите валидацию, которая будет выполняться только при создании объекта и никогда при модификации.
Пример класса-валидатора:
 package org.openxava.test.validators;
 
 import org.openxava.util.*;
 import org.openxava.validators.*;
 
 /**
 * @author Javier Paniza
 */
 
 public class ExcludeStringValidator implements IPropertyValidator { // 1
 
 private String string;
 
 public void validate(
 Messages errors, // 2
 Object value, // 3
 String objectName, // 4
 String propertyName) // 5
 throws Exception {
 if (value==null) return;
 if (value.toString().indexOf(getString()) >= 0) {
 errors.add("exclude_string", propertyName, objectName, getString());
 }
 }
 
 public String getString() {
 return string==null?"":string;
 }
 
 public void setString(String string) {
 this.string = string;
 }
 
 }
Давайте рассмотри валидатор подробнее. Валидатор должен реализовывать интерфейс IPropertyValidator (1), что вынуждает класс иметь метод validate(), где, собственно, и происходит проверка значения.
Параметры метода validate() следующие:
  1. Messages errors: Объек типа Messages, который представляет собой набор сообщений об ошибках, возникших при валидации. При создании своего валидатора вам необходимо добавлять сюда сообщения обо всех ошибках валидации, о которых вы хотите сообщить пользователю.
  2. Object value: Значение, которое мы будет проверять.
  3. String objectName: Имя объекта, к которому относится свойство, которое мы проверяем. Полезно для вывода в сообщения об ошибках.
  4. String propertyName: Имя свойства для валидации. Полезно для вывода в сообщения об ошибках.
Итак, если вы нашли ошибку (значение не прошло одну из проверок), вам необходимо сообщить об этом, добавив сообщение вызовом метода errors.add()). Для вызова данного метода необходимо задать идентификатор и агрументы сообщения. Идентификатор сообщения указывает на сообщение, определенное в ресурсном файле i18n вашего проекта. Вот пример определения сообщения:
 exclude_string={0} cannot contain {2} in {1}
Параметры {0}..{2} подменяются на аргументы, которые вы передаете в вызове errors.add().
Если идентфикатор сообщения не найдет в ресурсном файле, то вместо текста сообщения будет отображен его идентификатор.
Проверка значения (валидация) считается успешной, если ни одного сообщения не было добавлено в Messages errors. OpenXava собирает в единый список все сообщения от всех валидаторов перед сохранением объекта, и если список сообщений не пуст, то отображает данные сообщения и не позволяет пользователю сохранить объект.
В пакете org.openxava.validators вы найдете уже реализованные валидаторы.
Напомним, что @PropertyValidator определен как Hibernate Validator contraint (начиная с версии v3.0.1).

Проверка значения свойства для стереотипа или типа (Default validator) (начиная с версии v2.0.3)

Вы можете определить валидатор значения свойства на уровне типа или стереотипа свойства. Для этого необходимо в файле xava/validators.xml вашего проекта определить соответствующий валидатор.
К примеру, добавьте в файл xava/validators.xml вашего проекта следующее:
 <validators>
 <default-validator>
 <validator-class
 class="org.openxava.test.validators.PersonNameValidator"/>
 <for-stereotype stereotype="PERSON_NAME"/>
 </default-validator>
 </validators>
В этом примере мы привязываем валидатор PersonNameValidator к стереотипу PERSON_NAME. Теперь, если вы определите свойство на данном стереотипе:
 @Required @Stereotype("PERSON_NAME")
 private String name;
Значение данного свойства будет проверяться валидатором PersonNameValidator, несмотря на то, что само свойство не определяет никаких валидаторов. Валидатор PersonNameValidator будет применяться для всех свойств со стереотипом PERSON_NAME.

Примерно также вы можете создать валидатор для типа.
В файле validators.xml также можно определять валидаторы, выполняющиеся при проверке, представлено ли значение для обязательного свойства (аннотация @Required).
Также можно назначать имена (alias - алиасы) для классов-валидаторов.
Больше информации о валидаторах вы можете почерпнуть в файлах OpenXava/xava/validators.xml и OpenXavaTest/xava/validators.xml.
Обращаем ваше внимание, что валидаторы не сработают, если вы используете напрямую JPA api для сохранения ваших объектов в БД.

Ссылки (References)

Ссылки позволяют описать связь между сущностями. Ссылка реализуется как свойство (имеющее getter- и setter-методы), при этом тип данного свойства должен соответствовать одной из сущностей-моделей OX. Например, Customer может ссылаться на Seller, что позволит вам написать примерно такой код:
 Customer customer = ...
 customer.getSeller().getName();
Метод getName() в примере возвращает имя покупателя.
Синтаксическое определение ссылки таково:
 @Required // 1
 @Id // 2
 @SearchKey // 3 New in v3.0.2
 @DefaultValueCalculator // 4
 @ManyToOne( // 5
 optional=false // 1
 )
 private type referenceName; // 5
 public type getReferenceName() { ... } // 5
 public void setReferenceName(type newValue) { ... } // 5
  1. @ManyToOne(optional=false) (JPA), @Required (OX) (необязательная аннотация, рекомендуем использовать вариант JPA): Указывает, что ссылка является обязательной. В момент сохранения OpenXava проверяет, все ли обязательные свойства-ссылки заполнены. Если данное требование не выполнено, то сохранение прерывается и пользователю выводится список сообщений об ошибках.
  2. @Id (JPA, небязательная аннотация): Указывает, что свойсво-ссылка является частью ключа. Комбинация ключевых свойств и ссылочных свойств должна быть спроецирована на группу столбцов базы данных с уникальной кобинацией значений, обычно, это первичный ключ.
  3. @SearchKey (OX, необязательная аннотация): (Начиная с версии v3.0.2) Ключ поиска используется для поиска объектов пользователем в пользовательском интерфейсе. Редактируемые поля, ассоцириуемые с каждым свойством, доступным для поиска, в пользовательском интерфейсе позволяют вводить значения для поиска (фильтрации). По-умолчанию для реализации поиска OpenXava использует свойства, анотированные. Если же данные свойства скрыты, используется первое свойство в представлении. Анотация @SearchKey позволяет точно настоить поиск в пользовательском интерфейсе.
  4. @DefaultValueCalculator (OX, допускается только одно вхождение аннотации, необязательная аннотация): Реализует логику вычисления исходного (начального) значения свойства-ссылки. Класс-калькулятор, реализующий вычисление исходного значения, должен возвращать действительное значение, соответствующее реально существующему первичном ключу сущности, на которую реализуется ссылка. При этом, значение может быть простого типа, а может быть сложным объектом-ключом (если ключ реализуется как отдельный объект).
  5. Reference declaration (Определение свойства-ссылки): Обычное определение свойства на языке Java (закрытая (private) переменнная-член класса и getter- и setter-методы). Свойство-ссылка всегда имеет аннотацию @ManyToOne (JPA) и тип свойства всегда соответствует другой сущности.
Примеры ссылок:
 @ManyToOne
 private Seller seller; // 1
 public Seller getSeller() {
 return seller;
 }
 public void setSeller(Seller seller) {
 this.seller = seller;
 }
 
 @ManyToOne(fetch=FetchType.LAZY)
 private Seller alternateSeller; // 2
 public Seller getAlternateSeller() {
 return alternateSeller;
 }
 public void setAlternateSeller(Seller alternateSeller) {
 this.alternateSeller = alternateSeller;
 }
 
 
  1. Свойство-ссылка seller ссылается на сущность Seller.
  2. Свойство-ссылка alternateSeller также реализует ссылку на сущность Seller. В данном случае вы указываем режим выборки fetch=FetchType.LAZY, что означает, что данные из базы данных будут загружаться только по мере необходимости (когда к ним происходит обращение). Это более эффективный подход с точки зрения производительности, чем поведение JPA по-умолчанию. Мы советуем всегда использовать fetch=FetchType.LAZY при определении свойств-ссылок.
Если предположить, что выше приведённое определение ссылок-свойств было выполнено для сущности Customer, то можно написать следюущий код, использующий данные свойства:
 Customer customer = ...
 Seller seller = customer.getSeller();
 Seller alternateSeller = customer.getAlternateSeller();
 

Вычислитель исходного значения для ссылки (Default value calculator in references)

Для свойства-ссылки аннотация @DefaultValueCalculator работает совершенно также как для обычного свойства с той только разницей, что должно возвращаться значение, соответствующее реальной ссылке.
Для простого ключа определение калькулятора исходного значения может быть следующим:
 @ManyToOne(optional=false, fetch=FetchType.LAZY) @JoinColumn(name="FAMILY")
 @DefaultValueCalculator(value=IntegerCalculator.class, properties=
 @PropertyValue(name="value", value="2")
 )
 private Family family;
 
Метод вычисления calculate() может выглядеть так:
 public Object calculate() throws Exception {
 return new Integer(value);
 }
 
Из примера видно, что возвращается значение integer. Для свойства family будет возвращаться значение 2, определенное как @PropertyValue(name="value", value="2").

Определение сложного ключа (composed key) для сущности:
 @ManyToOne(fetch=FetchType.LAZY)
 @JoinColumns({
 @JoinColumn(name="ZONE", referencedColumnName="ZONE"),
 @JoinColumn(name="WAREHOUSE", referencedColumnName="NUMBER")
 })
 @DefaultValueCalculator(DefaultWarehouseCalculator.class)
 private Warehouse warehouse;
 
А теперь рассмотрим код калькулятора, который работает с композитным ключом:
 package org.openxava.test.calculators;
 
 import org.openxava.calculators.*;
 
 /**
 * @author Javier Paniza
 */
 public class DefaultWarehouseCalculator implements ICalculator {
 
 public Object calculate() throws Exception {
 WarehouseKey key = new WarehouseKey();
 key.setNumber(4);
 key.setZoneNumber(4);
 return key;
 }
 
 }
 
Класс возращает объект типа WarehouseKey.

Использование ссылок как ключей (Using references as key)

Свойство-ссылка может быть использована как ключ или часть ключа. Для этого необходимо добавить аннотацию @Id в определение свойства. Пример:
 @Entity
 @IdClass(AdditionalDetailKey.class)
 public class AdditionalDetail {
 
 // JoinColumn is also specified in AditionalDetailKey because
 // a bug in Hibernate, see http://opensource.atlassian.com/projects/hibernate/browse/ANN-361
 @Id @ManyToOne(fetch=FetchType.LAZY)
 @JoinColumn(name="SERVICE")
 private Service service;
 
 @Id @Hidden
 private int counter;
 
 ...
 
 }
Рассмотрим пример класса-ключа:
 public class AdditionalDetailKey implements java.io.Serializable {
 
 @ManyToOne(fetch=FetchType.LAZY)
 @JoinColumn(name="SERVICE")
 private Service service;
 
 @Hidden
 private int counter;
 
 // equals, hashCode, toString, getters and setters
 ...
 
 }
Вам придется создать ключевой класс, если вы используете составной ключ.
Мы рекомендуем использовать такой подход только, если вы работаете с уже существующей базой данных, структуру которой нельзя изменить. Если вы сами проектируйте схему хранения или способны на нее повлиять, лучше использовать автогенерируемые идентификаторы (autogenerated ids).

Коллекции объектов (Collections)

OpenXava позволяет работать с коллекциями ссылок на сущности. Коллекция объектов в OX - это ссылочное свойства с типом java.util.Collection.
Синтаксис коллекции объектов:
 @Size // 1
 @Condition // 2
 @OrderBy // 3
 @XOrderBy // 4
 @OneToMany/@ManyToMany // 5
 private Collection collectionName; // 5
 public Collection getCollectionName() { ... } // 5
 public void setCollectionName(Collection newValue) { ... } // 5
 
  1. @Size (HV, необязательная анотоация): Минимальное (min) и/ли максимальное (max) ожидаемое количество элементов. Это число проверяется перед сохранением в БД.
  2. @Condition (OX, необязательная аннотация): Является аналогом WHERE в SQL. Задает условие фильтрации объектов, которые должны быть включены в коллекцию.
  3. @OrderBy (JPA, необязательная аннотация): Задает порядок сортировки объектов в коллекции.
  4. @XOrderBy (OX, необязательная аннотация): Конструкция @OrderBy, которая является частью стандарта JPA, не позволяет использовать квалифицированные свойства (т.е. свойства с указанием сущности, к которой они относятся. Другое название - свойства ссылок - properties of references). Конструкция @XOrderBy позволяет это.
  5. Collection declaration (Определение коллекции): Обычное Java-определение переменной-члена класса с типом Collection и getter- и setter- методами. Коллекция должна иметь аннотацию @OneToMany (JPA) или @ManyToMany (JPA) и ссылаться на другую сущность.
Давайте рассмотрим несколько примеров:
 @OneToMany (mappedBy="invoice")
 private Collection deliveries;
 public Collection getDeliveries() {
 return deliveries;
 }
 public void setDeliveries(Collection deliveries) {
 this.deliveries = deliveries;
 }
В примере выше внутри сущности Invoice мы определили коллекцию объектов deliveries, ассоциированную с Invoice. Более подробная информация о объектно-релационных проекциях вы найдете в главе. В примере мы используем mappedBy="invoice", чтобы указать, что счет доставки (invoice of Delivery) используется для проецирования данной коллекции.
Посмотрим код, который использует такое определение коллекции:
 Invoice invoice = ...
 for (Delivery delivery: invoice.getDeliveries()) {
 delivery.doSomething();
 }
В примере для каждой доставки delivery из ассоциированных с данным счетом invoice выполяентся некий метод doSomething().
Рассмотрим более сложный пример, использующий ту же сущность Invoice:
 @OneToMany (mappedBy="invoice", cascade=CascadeType.REMOVE) // 1
 @OrderBy("serviceType desc") // 2
 @org.hibernate.validator.Size(min=1) // 3
 private Collection details;
  1. Используйте CascadeType.REMOVE, если вы хотите, чтобы вместе с головным объектом (invoice в нашем случае) удалялись дочерние объекты (details).
  2. Используйте @OrderBy, если выхотте, чтобы результат был отсортирован по в обратном порядке (desc) по serviceType.
  3. Ограничение @..Size(min=1) приводит к тому, что OX будет требовать, чтобы хотя бы 1 экземпляр объекта коллекции details был создан, чтобы основной объект invoice считался корректным.
С помощью конструкции @Condition вы можете полностью контролировать условия выборки данных для коллекции:
 @Condition(
 "${warehouse.zoneNumber} = ${this.warehouse.zoneNumber} AND " +
 "${warehouse.number} = ${this.warehouse.number} AND " +
 "NOT (${number} = ${this.number})"
 )
 public Collection getFellowCarriers() {
 return null;
 }
 
Если данная коллекция - часть сущности Carrier, приведённым условием в @Condition мы получим объекты carriers, расположенные на том же складе warehouse, но не. As you see you can use this in the condition in order to reference the value of a property of current object. Конструкция @Condition применима только к пользовательскому интерфейсу, создаваемому OpenXava. Если вы напрямую вызовите getFellowCarriers(), метод вернет null.
Если @Condition недостаточно, чтобы сформировать коллекцию в соответствие с бизнес-требованиями, вы можете самостоятельно перекрыть программный код, возращающий коллекцию. Предыдущий пример может быть реализован следующим образом:
 public Collection getFellowCarriers() {
 Query query = XPersistence.getManager().createQuery("from Carrier c where " +
 "c.warehouse.zoneNumber = :zone AND " +
 "c.warehouse.number = :warehouseNumber AND " +
 "NOT (c.number = :number) ");
 query.setParameter("zone", getWarehouse().getZoneNumber());
 query.setParameter("warehouseNumber", getWarehouse().getNumber());
 query.setParameter("number", getNumber());
 return query.getResultList();
 }
Как вы видете, это обычный getter-метод. Естественно, данный метод должен возвращать коллекцию java.util.Collection, элементами которой должны быть объкты типа Carrier.
Ссылки в коллекциях являются двунаправленными (bidirectional). Это означает, что если в объекте Seller определена коллекция customers, то в соответствующих объектах Customer необходимо иметь ссылку на соответствующий объект Seller.
Кроме того, возможно в объекте Customer иметь больше одной ссылки на объекты Seller (например, seller и alternateSeller). В этом случае JPA не может понять, какое свойство использовать, поэтому появляется аттрибут mappedBy у аннотации @OneToMany. Приведем пример использования:
 @OneToMany(mappedBy="seller")
 private Collection customers;
Такой вариант записи позволяет точно указать, что для данной коллекции используется свойство seller, а не alternateSeller.

Аннотация @ManyToMany (JPA) (Многие ко Многим) позволяет реализовать связь "Многие ко Многим"(many-to-many multiplicity). Рассмотрим пример:
 @Entity
 public class Customer {
 ...
 @ManyToMany
 private Collection<State> states;
 ...
 }
 
В данном примере покупатель Customer связан с коллекцией штатов/государств states, но также State может быть связан с несколькими покупателями (customers).

Методы (Methods)

Методы определяются как обычно. Класс OpenXava-сущность (на самом деле, JPA entity) - это самый обычный класс Java. Пример:
 public void increasePrice() {
 setUnitPrice(getUnitPrice().multiply(new BigDecimal("1.02")).setScale(2));
 }
Методы - это основа объектнов, т.к. без них объект всего лишь обёртка данных. Мы рекомендуем всегда, когда это возможно, реализовывать бизнес-логику внутри методов (model layer), а не действий (controller layer).

Методы для поиска в БД (Finders)

Метод для поиска в БД - это узкоспециализированный статический метод, предназначенный для поиска одного объекта или коллекции экземпляров объектов, удовлетворяющих определенным критериям.
Несколько примеров:
 public static Customer findByNumber(int number) throws NoResultException {
 Query query = XPersistence.getManager().createQuery(
 "from Customer as o where o.number = :number");
 query.setParameter("number", number);
 return (Customer) query.getSingleResult();
 }
 
 public static Collection findAll() {
 Query query = XPersistence.getManager().createQuery("from Customer as o");
 return query.getResultList();
 }
 
 public static Collection findByNameLike(String name) {
 Query query = XPersistence.getManager().createQuery(
 "from Customer as o where o.name like :name order by o.name desc");
 query.setParameter("name", name);
 return query.getResultList();
 }
Теперь несколько примеров использования методов для поиска:
 Customer customer = Customer.findByNumber(8);
 Collection javieres = Customer.findByNameLike(%JAVI%);
Обращаем ваше внимание, что использование методов для поиска позволяет улучшить читаемость создаваемого кода в сравнении с многословными запросами к API стандарта JPA. Но это всего лишь рекомендация к стилю программирования, никто не запрещает вам напрямую использовать запросы к JPA.

Проверка для перечислимого типа (Entity validator)

Аннотация @EntityValidator предназначена для реализации проверок на уровне сущности (model level). Данная аннотация должна использоваться для общих проверок - когда выполняется валидация несколько свойств одновременно и данные проверки не могут быть логически соотносены к конкретным свойствам.
Синтаксис:
 @EntityValidator(
 value=class, // 1
 onlyOnCreate=(true|false), // 2
 properties={ @PropertyValue ... } // 3
 )
 
  1. value (обязательный аттрибут): Класс, который реализует логику проверки (класс-валидатор). Данный класс должен реализовывать интерфейс IValidator.
  2. onlyOnCreate (необязательный аттрибут): Если данный аттррибут равен true, то проверка будет выполняться только в момент создания нового объекта. но не будет выполнятся в момент изменения уже существующего объекта. Значение по-умолчанию false.
  3. properties (несколько @PropertyValue, необязательный аттрибут): Предоставляет возможность установить значения свойств-параметров вызова класса-валидатора.
Пример:
 @EntityValidator(value=org.openxava.test.validators.CheapProductValidator.class, properties= {
 @PropertyValue(name="limit", value="100"),
 @PropertyValue(name="description"),
 @PropertyValue(name="unitPrice")
 })
 public class Product {
Код класса-валидатора для примера выше:
 package org.openxava.test.validators;
 
 import java.math.*;
 
 /**
 * @author Javier Paniza
 */
 
 public class CheapProductValidator implements IValidator { // 1
 
 private int limit;
 private BigDecimal unitPrice;
 private String description;
 
 public void validate(Messages errors) { // 2
 if (getDescription().indexOf("CHEAP") >= 0 ||
 getDescription().indexOf("BARATO") >= 0 ||
 getDescription().indexOf("BARATA") >= 0) {
 if (getLimiteBd().compareTo(getUnitPrice()) < 0) {
 errors.add("cheap_product", getLimitBd()); // 3
 }
 }
 }
 
 public BigDecimal getUnitPrice() {
 return unitPrice;
 }
 
 public void setUnitPrice(BigDecimal decimal) {
 unitPrice = decimal;
 }
 
 public String getDescription() {
 return description==null?"":description;
 }
 
 public void setDescription(String string) {
 description = string;
 }
 
 public int getLimit() {
 return limit;
 }
 
 public void setLimit(int i) {
 limit = i;
 }
 
 private BigDecimal getLimitBd() {
 return new BigDecimal(Integer.toString(limit));
 }
 
 }
 
Класс-валидатор должен реализовывать интерфейс IValidator (1), что принуждает к реализации метода validate(Messages messages) (2). В данном методе выполняются проверки, если проверки не проходят (выполняются с ошибкой), то вызывается errors.add(..) (3), используя идентификаторы сообщений из ресурсного фалйаi18n и параметров для подстановки в сообщение. Если процесс валидации выполняется с ошибкой (данный процесс сопровождается вызовом всех зарегистрированных валидаторов), то OpenXava не сохраняет объект и выводит сообщения об ошибках пользователю (созданные внутри метода validate()).
В нашем примере выполняется общая проверка для свойств description и unitPrice. В таких случаях рекомендуется выполнять проверку на уровне сущности (model level).
Для одной сущности можно реализовать несколько @EntityValidators. Пример:
 @EntityValidators({
 @EntityValidator(value=org.openxava.test.validators.CheapProductValidator.class, properties= {
 @PropertyValue(name="limit", value="100"),
 @PropertyValue(name="description"),
 @PropertyValue(name="unitPrice")
 }),
 @EntityValidator(value=org.openxava.test.validators.ExpensiveProductValidator.class, properties= {
 @PropertyValue(name="limit", value="1000"),
 @PropertyValue(name="description"),
 @PropertyValue(name="unitPrice")
 }),
 @EntityValidator(value=org.openxava.test.validators.ForbiddenPriceValidator.class,
 properties= {
 @PropertyValue(name="forbiddenPrice", value="555"),
 @PropertyValue(name="unitPrice")
 },
 onlyOnCreate=true
 )
 })
 public class Product {
 
@EntityValidator определен как Hibernate Validator contraint (начиная с версии v3.0.1).

Проверка при удалении (Remove validator)

Аннотация @RemoveValidator также реализован на уровне сущности (model level) и предназначен для выполнения проверки перед удалением объекта. Данная проверка позволяет запретить удаление объекта.
Синтаксис:
 @RemoveValidator(
 value=class, // 1
 properties={ @PropertyValue ... } // 2
 )
 
  1. class (обязательный аттрибут): Класс, реализующий логику проверки. Должен реализовывать интерфейс IRemoveValidator.
  2. properties (несколько определений @PropertyValue, необязательный аттрибут): Используется для передачи параметров валидатору, перед его выполнением.
Пример использования такого валидатора:
 @RemoveValidator(value=DeliveryTypeRemoveValidator.class,
 properties=@PropertyValue(name="number")
 )
 public class DeliveryType {
 
Пример кода самого валидатора:
 package org.openxava.test.validators;
 
 import org.openxava.test.model.*;
 import org.openxava.util.*;
 import org.openxava.validators.*;
 
 /**
 * @author Javier Paniza
 */
 public class DeliveryTypeRemoveValidator implements IRemoveValidator { // 1
 
 private DeliveryType deliveryType;
 private int number; // We use this (instaed of obtaining it from deliveryType)
 // for testing @PropertyValue for simple properties
 
 public void setEntity(Object entity) throws Exception { // 2
 this.deliveryType = (DeliveryType) entity;
 }
 
 public void validate(Messages errors) throws Exception {
 if (!deliveryType.getDeliveries().isEmpty()) {
 errors.add("not_remove_delivery_type_if_in_deliveries", new Integer(getNumber())); // 3
 }
 }
 
 public int getNumber() {
 return number;
 }
 
 public void setNumber(int number) {
 this.number = number;
 }
 
 }
 
Класс-валидатор должен реализовывать интерфейс IRemoveValidator (1) , что принуждает реализовывать метод setEntity() (2), который получает объект для удаления. Если при выполнении метода validate() (3) в объект Messages будет добавлено одно или более сообщение об ошибке, то считается, что проверка выполнилась с ошибкой. Если после выполнения всех проверок есть хотя бы одна ошибка валидации, то OpenXava не удаляет объект и выводит сообщения с ошибками валидации пользователю.
В нашем примере валидатор проверяет, есть ли доставки (deliveries), которые используют данный тип доставки (delivery type) прежде, чем удалить данный тип доставки.
Также как в случае с @EntityValidator, можно использовать несколько @RemoveValidator для сущности, обернув их аннтоацией @RemoveValidators.
@RemoveValidator выполняется в момент, когда пользователь пытается удалить сущность средствами OpenXava (использование MapFacade или стандартных действий (actions) OX), но не будет выполняться, если вы попытаетесь напрямую использоваться вызовы JPA. При прямом использовании JPA для создания констрейна на удаление используйте аннотацию @PreRemove JPA.

Методы обратного вызова JPA (JPA callback methods)

@PrePersist - контролируемая вставка данных в БД

Используя аннотацию @PrePersist, вы можете встроить свою собственную логику в процесс выполнения сохранения объекта в БД.
Пример:
 @PrePersist
 private void prePersist() {
 setDescription(getDescription() + " CREATED");
 }
В нашем примере, каждый раз при создании сущности DeliveryType в свойство description добавляется суффикс.

@PreUpdate - контролируемое изменение данных в БД

Используя аннотацию @PreUpdate, вы можете встроить свою собственную логику, которая будет выполняться после перехода сущности в определенное состояние или перед тем, как сохранять изменения сущности в БД, т.е., просто перед выполнением выражения UPDATE в БД.
Пример:
 @PreUpdate
 private void preUpdate() {
 setDescription(getDescription() + " MODIFIED");
 }
В этом примере при люьбом изменении DeliveryType в описание добавляется суффикс.
Как вы наверное уже убедились, логика использования @PreUpdate аналогична @PrePersist, разница только в моменте, когда соответствующий класс вызывается.

Другие методы обратного вызова JPA

Аналогично выше описанным аннотациям вы можете использовать остальные методы обратного вызова, определенные в стандарте JPA: @PrePersist, @PostPersist, @PreRemove, @PostRemove, @PreUpdate, @PostUpdate and @PostLoad.

Встраиваемые классы (Embeddable classes)

Как сказано в спецификации JPA:
"Сущность может использовать другие хорошо спроектированные классы для отображения состояния сущности. Экземпляры данных классов, в отличие от собственно экземпляров сущности, не должны самостоятельно отображать сохраняемую сущность (do not have persistent identity). В противоположность, они существуют только как встраиваемые объекты той сущности, к которой они относятся. Такие встраиваемые объекты должны строго принадлежат сущности-владельцу и не должны использоваться несколькими сохраняемыми сущностями (are not sharable across persistent entities)."
Оригинальный текст:
("An entity may use other fine-grained classes to represent entity state. Instances of these classes, unlike
entity instances themselves, do not have persistent identity. Instead, they exist only as embedded objects
of the entity to which they belong. Such embedded objects belong strictly to their owning entity, and are
not sharable across persistent entities.")
Синтраксис встраиваемого класса:
 @Embeddable // 1
 public class EmbeddableName { // 2
 // Properties // 3
 // References // 4
 // Methods // 5
 }
 
  1. @Embeddable (JPA, одно вхождение, обязательная аннотация): Указывает на то, что класс является встраиваемым классом JPA. Иными словами, экземпляры данного класса будут частями сохраняемых объектов.
  2. Class declaration: Обычное определение класса Java. Можно использовать наследование (оператор extends) и реализовывать интерфейсов (оператор implements).
  3. Properties: Обычные свойства (переменные-члены класса и getter- /setter- методы) Java.
  4. References: Ссылки на другие сущности. Данная конструкция не поддерживается стандратом JPA 1.0 (EJB 3.0), но реализация Hibernate поддерживает это.
  5. Methods: Методы, реализующие бизнес-логику.

Встроенная ссылка (Embedded reference)

Возможно использование встраиваемых свойств-ссылок.
В рассматриваемом далее примере определяется класс Address (аннотированный с помощью @Embedded), на который ссылается основной класс-сущность. В основной сущности встроенная ссылка определяется следующим образом:
 @Embedded
 private Address address;
 
Сам класс Address необходимо определить как встраиваемый:
 package org.openxava.test.model;
 
 import javax.persistence.*;
 import org.openxava.annotations.*;
 
 /**
 *
 * @author Javier Paniza
 */
 
 @Embeddable
 public class Address implements IWithCity { // 1
 
 @Required @Column(length=30)
 private String street;
 
 @Required @Column(length=5)
 private int zipCode;
 
 @Required @Column(length=20)
 private String city;
 
 // ManyToOne inside an Embeddable is not supported by JPA 1.0 (see at 9.1.34),
 // but Hibernate implementation supports it.
 @ManyToOne(fetch=FetchType.LAZY, optional=false) @JoinColumn(name="STATE")
 private State state; // 2
 
 public String getCity() {
 return city;
 }
 
 public void setCity(String city) {
 this.city = city;
 }
 
 public String getStreet() {
 return street;
 }
 
 public void setStreet(String street) {
 this.street = street;
 }
 
 public int getZipCode() {
 return zipCode;
 }
 
 public void setZipCode(int zipCode) {
 this.zipCode = zipCode;
 }
 
 public State getState() {
 return state;
 }
 
 public void setState(State state) {
 this.state = state;
 }
 
 }
Как вы видете, встраиваемый класс может реализовывать интерфейс (1), содержать свойства-ссылки (2) и т.д., но не может использовать сохранямые коллекции или вызывть методы обратного вызова JPA.
Использовать объект Address можно следующим образом (если нам необходимо прочитать его):
 Customer customer = ...
 Address address = customer.getAddress();
 address.getStreet(); // to obtain the value
Другой вариант работы с объектом Address:
 // to set a new address
 Address address = new Address();
 address.setStreet(“My street”);
 address.setZipCode(46001);
 address.setCity(“Valencia”);
 address.setState(state);
 customer.setAddress(address);
В нашем примере мы использовали простую ссылку на Address (не коллекцию), и генерироваться будет код простого JavaBean, чей жизненный цикл будет ассоцирован с объектом-контейнером. Т.е. Address добавляется и удаляется только через контейнер Customer. Еще раз обращаем ваше внимание на то, что встраиваемый класс Address не имеет своего жизненного цикла и не может разделяться между несколькими (использоваться другими) Customer.

Встраиваемые коллекции (Embedded collections)

Встраиваемые коллекции не поддерживаются JPA 1.0. Тем не менее, разработчик сам может реализовать их, используя коллекции сущностей и каскадные типы (cascade type) REMOVE или ALL. OpenXava обрабатывает такие коллекции специальным способом так, как если бы они были встраиваемыми коллекциями.
Рассмотрим выше сказанное на примере. В сущности-контейнере (в нашем примере, это Invoice) необходимо определить следующую коллекцию:
 @OneToMany (mappedBy="invoice", cascade=CascadeType.REMOVE)
 private Collection details;
 
Обращаем ваше внимание на каскадный тип CascadeType.REMOVE и на то, что InvoiceDetail - это сущность, а не встраиваемый класс:
 package org.openxava.test.model;
 
 import java.math.*;
 
 import javax.persistence.*;
 
 import org.hibernate.annotations.Columns;
 import org.hibernate.annotations.Type;
 import org.hibernate.annotations.Parameter;
 import org.hibernate.annotations.GenericGenerator;
 import org.openxava.annotations.*;
 import org.openxava.calculators.*;
 import org.openxava.test.validators.*;
 
 /**
 *
 * @author Javier Paniza
 */
 
 @Entity
 @EntityValidator(value=InvoiceDetailValidator.class,
 properties= {
 @PropertyValue(name="invoice"),
 @PropertyValue(name="oid"),
 @PropertyValue(name="product"),
 @PropertyValue(name="unitPrice")
 }
 )
 public class InvoiceDetail {
 
 @ManyToOne // Lazy fetching produces a fails on removing a detail from invoice
 private Invoice invoice;
 
 @Id @GeneratedValue(generator="system-uuid") @Hidden
 @GenericGenerator(name="system-uuid", strategy = "uuid")
 private String oid;
 
 private ServiceType serviceType;
 public enum ServiceType { SPECIAL, URGENT }
 
 @Column(length=4) @Required
 private int quantity;
 
 @Stereotype("MONEY") @Required
 private BigDecimal unitPrice;
 
 @ManyToOne(fetch=FetchType.LAZY, optional=false)
 private Product product;
 
 @DefaultValueCalculator(CurrentDateCalculator.class)
 private java.util.Date deliveryDate;
 
 @ManyToOne(fetch=FetchType.LAZY)
 private Seller soldBy;
 
 @Stereotype("MEMO")
 private String remarks;
 
 @Stereotype("MONEY") @Depends("unitPrice, quantity")
 public BigDecimal getAmount() {
 return getUnitPrice().multiply(new BigDecimal(getQuantity()));
 }
 
 public boolean isFree() {
 return getAmount().compareTo(new BigDecimal("0")) <= 0;
 }
 
 @PostRemove
 private void postRemove() {
 invoice.setComment(invoice.getComment() + "DETAIL DELETED");
 }
 
 public String getOid() {
 return oid;
 }
 public void setOid(String oid) {
 this.oid = oid;
 }
 public ServiceType getServiceType() {
 return serviceType;
 }
 public void setServiceType(ServiceType serviceType) {
 this.serviceType = serviceType;
 }
 public int getQuantity() {
 return quantity;
 }
 public void setQuantity(int quantity) {
 this.quantity = quantity;
 }
 public BigDecimal getUnitPrice() {
 return unitPrice==null?BigDecimal.ZERO:unitPrice;
 }
 public void setUnitPrice(BigDecimal unitPrice) {
 this.unitPrice = unitPrice;
 }
 
 public Product getProduct() {
 return product;
 }
 
 public void setProduct(Product product) {
 this.product = product;
 }
 
 public java.util.Date getDeliveryDate() {
 return deliveryDate;
 }
 
 public void setDeliveryDate(java.util.Date deliveryDate) {
 this.deliveryDate = deliveryDate;
 }
 
 public Seller getSoldBy() {
 return soldBy;
 }
 
 public void setSoldBy(Seller soldBy) {
 this.soldBy = soldBy;
 }
 
 public String getRemarks() {
 return remarks;
 }
 
 public void setRemarks(String remarks) {
 this.remarks = remarks;
 }
 
 public Invoice getInvoice() {
 return invoice;
 }
 
 public void setInvoice(Invoice invoice) {
 this.invoice = invoice;
 }
 
 }
В примере был представлен класс, описывающий сложную сущность, с калькуляторами, валидаторами, ссылками и прочие. Кроме этого вы можете здесь определить ссылку на класс-контейнер (invoice). Если вы это сделаете это, то при удалении Invoice будут удаляться все InvoiceDetail. Также наличие такой ссылки влияет на генерируемый пользовательский интерфейс (более подробную информацию вы найдете в главе Представление).

Наследование (Inheritance)

OpenXava поддерживает наследование Java и наследование JPA.
Рассмотрим пример. Мы можем использовать @MappedSuperclass следующим образом:
 package org.openxava.test.model;
 
 import javax.persistence.*;
 
 import org.hibernate.annotations.*;
 import org.openxava.annotations.*;
 
 /**
 * Base class for defining entities with a UUID oid. <p>
 *
 * @author Javier Paniza
 */
 
 @MappedSuperclass
 public class Identifiable {
 
 @Id @GeneratedValue(generator="system-uuid") @Hidden
 @GenericGenerator(name="system-uuid", strategy = "uuid")
 private String oid;
 
 public String getOid() {
 return oid;
 }
 
 public void setOid(String oid) {
 this.oid = oid;
 }
 
 }
 
Можно использовать несколько уровней наследования. Т.к. можно определить аттрибут @MappedSuperclass для класса, который уже наследует другой класс:
 package org.openxava.test.model;
 
 import javax.persistence.*;
 
 import org.openxava.annotations.*;
 
 /**
 * Base class for entities with a 'name' property. <p>
 *
 * @author Javier Paniza
 */
 @MappedSuperclass
 public class Nameable extends Identifiable {
 
 @Column(length=50) @Required
 private String name;
 
 public String getName() {
 return name;
 }
 
 public void setName(String name) {
 this.name = name;
 }
 
 }
 
Теперь мы можем использовать оба класса (и Identifiable, и Nameable), чтобы определять экземпляры:
 package org.openxava.test.model;
 
 import javax.persistence.*;
 
 /**
 *
 * @author Javier Paniza
 */
 
 @Entity
 @DiscriminatorColumn(name="TYPE")
 @DiscriminatorValue("HUM")
 @Table(name="PERSON")
 @AttributeOverrides(
 @AttributeOverride(name="name", column=@Column(name="PNAME"))
 )
 public class Human extends Nameable {
 
 @Enumerated(EnumType.STRING)
 private Sex sex;
 public enum Sex { MALE, FEMALE };
 
 public Sex getSex() {
 return sex;
 }
 public void setSex(Sex sex) {
 this.sex = sex;
 }
 
 }
 
Наконец определим рельную сущность на базе выше перечисленных классов:
 package org.openxava.test.model;
 
 import javax.persistence.*;
 
 /**
 *
 * @author Javier Paniza
 */
 
 @Entity
 @DiscriminatorValue("PRO")
 public class Programmer extends Human {
 
 @Column(length=20)
 private String mainLanguage;
 
 public String getMainLanguage() {
 return mainLanguage;
 }
 
 public void setMainLanguage(String mainLanguage) {
 this.mainLanguage = mainLanguage;
 }
 
 }
 
Совет: Вы можете создать модуль OpenXava для Human (Человке) и Programmer (Программист), а не для классов Identifiable или Nameble напрямую). В модуле Programmer module пользователь получить доступ только к "программистам". А в модуле Human пользователь сможет работать со всеми объектами Human и Programmer. Более того, когда пользователь попытается просмотреть карточку "программиста" из модуля Human module, именно представление Programmer будет отображено. Это то, что я называю, настоящим полиформизмом.
Остановимся на проецировании - аннотация @AttributeOverrides поддерживается, но, на настоящий момент, только стратегия иерархии "table" = "class" работает корректно.

Составной ключ (Composite key)

Предпочтительный способ определения ключа в таблице (в сущности) - это простой автогенерируемый ключ (почитайте про аннотации @Id и @GeneratedValue), но иногда, чаще всего, когда вы работаете с уже существующей базой данных, на структуру которой вы не можете повлиять, вам небходимо проецировать сущность в таблицу, в которой ключ состоит из нескольких стобцов. Данный случай может быть реализован в JPA (и, соответственно, в OpenXava) двумя путями: используя одну из аннтоаций @IdClass или @EmbeddedId

Класс Id (Id class)

В этом подходе аннотация @IdClass внутри вашей сущности указывает на ключевой класс. В этом случае свойства-части ключа описываются прямо в вашей сущности, при этом вам необходимо промаркировать их аннотацией @Id:
 package org.openxava.test.model;
 
 import javax.persistence.*;
 
 import org.openxava.annotations.*;
 import org.openxava.jpa.*;
 
 /**
 *
 * @author Javier Paniza
 */
 
 @Entity
 @IdClass(WarehouseKey.class)
 public class Warehouse {
 
 @Id
 // Column is also specified in WarehouseKey because a bug in Hibernate, see
 // http://opensource.atlassian.com/projects/hibernate/browse/ANN-361
 @Column(length=3, name="ZONE")
 private int zoneNumber;
 
 @Id @Column(length=3)
 private int number;
 
 @Column(length=40) @Required
 private String name;
 
 
 public String getName() {
 return name;
 }
 
 public void setName(String name) {
 this.name = name;
 }
 
 public int getNumber() {
 return number;
 }
 
 public void setNumber(int number) {
 this.number = number;
 }
 
 public int getZoneNumber() {
 return zoneNumber;
 }
 
 public void setZoneNumber(int zoneNumber) {
 this.zoneNumber = zoneNumber;
 }
 
 }
 
 
Затем вам необходимо определить ваш id-класс, который представляет собой обычный сериализирумый (serializable) класс, который содержит все ключевые свойства вашей сущности-контейнера:
 package org.openxava.test.model;
 
 import java.io.*;
 
 import javax.persistence.*;
 
 
 /**
 *
 * @author Javier Paniza
 */
 
 public class WarehouseKey implements Serializable {
 
 @Column(name="ZONE")
 private int zoneNumber;
 private int number;
 
 @Override
 public boolean equals(Object obj) {
 if (obj == null) return false;
 return obj.toString().equals(this.toString());
 }
 
 @Override
 public int hashCode() {
 return toString().hashCode();
 }
 
 @Override
 public String toString() {
 return "WarehouseKey::" + zoneNumber+ ":" + number;
 }
 
 public int getNumber() {
 return number;
 }
 
 public void setNumber(int number) {
 this.number = number;
 }
 
 public int getZoneNumber() {
 return zoneNumber;
 }
 
 public void setZoneNumber(int zoneNumber) {
 this.zoneNumber = zoneNumber;
 }
 
 }

Встраиваемый Id (Embedded id)

Чтобы использовать встраеваемый Id, необходимо использовать аннотацию @EmbeddedId. Определение же такого класса делается аннотацией @Embeddable . В отличие от использования @IdClass вам не нужно описывать свойства-части ключа в основной сущности.
 package org.openxava.test.model;
 
 import javax.persistence.*;
 
 import org.openxava.annotations.*;
 
 /**
 *
 * @author Javier Paniza
 */
 
 @Entity
 public class Warehouse {
 
 @EmbeddedId
 private WarehouseKey key;
 
 @Column(length=40) @Required
 private String name;
 
 public WarehouseKey getKey() {
 return key;
 }
 
 public void setKey(WarehouseKey key) {
 this.key = key;
 }
 
 public String getName() {
 return name;
 }
 
 public void setName(String name) {
 this.name = name;
 }
 
 }
 
Рассмотрим сам встраиваемый класс, который содержит ключевые свойства:
 package org.openxava.test.model;
 
 import javax.persistence.*;
 
 /**
 *
 * @author Javier Paniza
 */
 
 @Embeddable
 public class WarehouseKey implements java.io.Serializable {
 
 
 @Column(length=3, name="ZONE")
 private int zoneNumber;
 
 @Column(length=3)
 private int number;
 
 public int getNumber() {
 return number;
 }
 
 public void setNumber(int number) {
 this.number = number;
 }
 
 public int getZoneNumber() {
 return zoneNumber;
 }
 
 public void setZoneNumber(int zoneNumber) {
 this.zoneNumber = zoneNumber;
 }
 
 }

Стандартный метод проверки Hibernate (Hibernate Validator) (начиная с v3.0.1)

OpenXava полность поддерживает возможности Hibernate Validator. Вы можете создавать свои собственные ограничения (constrains) для ваших сушностей (обратитесь к документации Hibernate Validator documention за более подробной информацией) и OpenXava расспознает их и будет выводить соответствующие сообщения, если валидация не пройдет, пользователю.
Более того, аннотации OpenXava @Required, @PropertyValidator и @EntityValidator сами по себе определены как Hibernate Validator constraints, что означает, что если вы будет использовать прямоц доступ к JPA, данные валидации будут корректно работать.
С другой стороны аннотации @RemoveValidator, @PropertyValidator(onlyOnCreate=true), EntityValidator(onlyOnCreate=true) и Метод проверки исходного значения реализованы только в OpenXava и не распознаются Hibernate Validator или JPA.